﻿//#define TRACE_XML

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Xml;
using System.IO;
using System.Diagnostics;
using System.Text;
using System.Windows.Media.Imaging;
using System.Net.Sockets;

namespace C4Tools
{
    // TODO: Convert to a fully asynchronous operational model.
    // C4Manager should expose BeginInvoke and EndInvoke methods for
    // each synchronous method, and the synch methods should be
    // implemented using the asynch methods.
    //
    // This means using a background thread, registering this app as an
    // "Event listener" for Control4 stuff, and correlating requests and
    // replies using sequence numbers.
    //
    // It also means keeping in memory a fairly complete model of the
    // system, including floors, rooms, devices located in each room,
    // and the state of the various devices.
    //
    // This will allow, for example, executing a PlayMovieInRoom method
    // correctly - it will turn on the device as neessary, select the
    // output video device, select the movie media, then hit the play
    // button.
    //
    // Also need to be able to do

    class C4Manager
    {
        private static readonly int DEFAULT_C4SOAP_PORT = 5020;

        byte[] nullByte;
        Int32 port;
        string c4server;
        TcpClient client;
        NetworkStream stream;
        public int connectionStatus;

        XmlReaderSettings xmlrs = new XmlReaderSettings
        {
            ProhibitDtd = true
        };
        XmlWriterSettings xmlws = new XmlWriterSettings
        {
            Indent = true,
            IndentChars = "  ",
            OmitXmlDeclaration = true
        };

        public C4Manager(string controller)
        {
            this.nullByte = new byte[] { 0 };
            this.port = DEFAULT_C4SOAP_PORT;
            this.c4server = controller;
            this.client = new TcpClient(this.c4server, this.port);

            // Get a client stream for reading and writing to that ip+port
            //  Stream stream = client.GetStream();
            this.stream = client.GetStream();
            this.stream.ReadTimeout = 40; // ms
        }


        public bool IsConnected
        {
            get
            {
                if (connectionStatus == 0)
                {
                    string message = "<c4soap name='GetVersionInfo' async='False'/>";
                    string response = RawSend(message);
                    connectionStatus = response.IndexOf("Director");
                }
                return (connectionStatus != -1);
            }
        }



        public List<int> GetDiscChangerIds()
        {
            // command to get devices and capabilities
            string cmdString1 = String.Format(Constants.GetItemsCmdTemplate, 6);
            string response1 = RawSend(cmdString1);

            XElement root = XElement.Parse(response1);
            List<int> changers = root.Element("systemitems")
                .Elements("item")
                .Where(item => {
                        XElement capabilities = item.Element("devicedata").Element("capabilities");
                        if (capabilities == null) return false;
                        XElement playsDvds = capabilities.Element("can_play_dvd");
                        if (playsDvds == null) return false;
                        if (playsDvds.Value != "True") return false;
                        XElement mediaCount = capabilities.Element("media_count");
                        if (mediaCount == null) return false;
                        return (Int32.Parse(mediaCount.Value) > 1);
                    })
                .Select(elt => Int32.Parse(elt.Element("subitems")
                                           .Element("item")
                                           .Element("id")
                                           .Value))
                .ToList();
            return changers;
        }


        private static string GetMovieListCmd(int id)
        {
            return String.Format(Constants.GetMoviesCmdTemplate, id);
        }


        public List<C4Room> GetRooms()
        {
            string cmdString1 = String.Format(Constants.GetItemsCmdTemplate, 1);
            string response1 = RawSend(cmdString1);

            XElement root = XElement.Parse(response1);
            List<C4Room> rooms = root.Element("systemitems")
                .Element("item")
                .Element("subitems")
                .Element("item")
                .Element("subitems")
                .Element("item")
                .Element("subitems")
                .Elements("item")
                .Elements("subitems")
                .Elements("item")
                .Where(item =>
                    {
                        XElement itemType = item.Element("type");
                        if (itemType == null) return false;
                        return (itemType.Value == "8");
                    })
                .Select(elt => new C4Room
                    {
                        Name = elt.Element("name").Value,
                        Id = Int32.Parse(elt.Element("id").Value)
                    })
                .ToList();
            return rooms;
        }


        public List<C4Movie> GetDiscChangerMovies()
        {
            var changers = this.GetDiscChangerIds();
            int i = 1;
            var list = new List<C4Movie>();
            changers.ForEach(id => {
                    var range = this.GetMoviesOnChanger(id,i++);
                    list.AddRange(range);
                });
            return list;
        }


        public List<C4Movie> GetMoviesOnChanger(int deviceId, int index)
        {
            var cmd = C4Manager.GetMovieListCmd(deviceId);
            string response = RawSend(cmd);
            string p = index.ToString() + "/";
            XElement root = XElement.Parse(response);
            List<C4Movie> movies = root.Element("movies")
                .Elements("movie")
                .Select(elt => new C4Movie
                    {
                        Title = elt.Element("title").Value,
                        Genre = elt.Element("genre").Value,
                        Rating = elt.Element("rating").Value,
                        ReleaseDate = elt.Element("release_date").Value,
                        Cast = elt.Element("cast").Value,
                        Directors = elt.Element("directors").Value,
                        Id = Int32.Parse(elt.Element("id").Value),
                        DeviceId = deviceId,
                        Location = p + elt.Element("location").Value,
                        CoverLocation = "file://" + this.c4server + "/" +
                                        "media/images/movie/" +
                                        elt.Element("img").Value +
                                        "_m.jpg"
                    })
                .ToList();
            return movies;
        }


        public void PlayMovieInRoom(C4Movie movie, C4Room room)
        {
            // TODO: modify this to turn on the output device, in case
            // it is not on.

            // select the desired DVD player
            var cmd = String.Format(Constants.SelectVideoDeviceTemplate,
                                    room.Id,
                                    movie.DeviceId);
            string response = RawSend(cmd);

            // select the desired video media on the selected DVD player
            cmd = String.Format(Constants.SelectMediaTemplate,
                                room.Id,
                                movie.Id);
            response = RawSend(cmd);

            // PLAY
            cmd = String.Format(Constants.ControlCmdTemplate,
                                room.Id, "PLAY");
            response = RawSend(cmd);
        }


        /// <summary>
        ///   Send one c4soap message to the controller.
        /// </summary>
        /// <param name='message'>xxx</param>
        /// <returns>string</returns>
        /// <remarks>
        ///   <para>
        ///     The caller is responsible for formatting the message.
        ///   </para>
        /// </remarks>
        public string RawSend(string message)
        {
            bool done = false;
            var trimmed = message.Replace("\r\n","")
                .Replace("  <", "<");

            TraceInputXml(trimmed);

            // Translate the passed message into ASCII and store it as a Byte array.
            byte[] data = System.Text.Encoding.ASCII.GetBytes(trimmed);

            // Send the message to the connected TcpServer.
            this.stream.Write(data, 0, data.Length);
            this.stream.Write(this.nullByte, 0, nullByte.Length);

            // Receive the TcpServer.response
            data = new Byte[1024];
            int n;
            var sb = new StringBuilder();
            while (!done)
            {
                try
                {
                    while ((n = stream.Read(data, 0, data.Length)) > 0)
                    {
                        // check for last byte
                        if (data[n-1] == 0)
                        {
                            sb.Append(System.Text.Encoding.ASCII.GetString(data, 0, n-1));
                            done = true;
                            break;
                        }
                        sb.Append(System.Text.Encoding.ASCII.GetString(data, 0, n));
                    }
                }
                catch (System.IO.IOException)
                {
                    // timeout - A-OK
                }
            }

            var response = sb.ToString();
            TraceOutputXml(response);
            return response;
        }



        [Conditional("TRACE_XML")]
        private void TraceInputXml(string command)
        {
            TraceXmlParcel(command, "input");
        }

        [Conditional("TRACE_XML")]
        private void TraceOutputXml(string response)
        {
            TraceXmlParcel(response, "output");
        }

        [Conditional("TRACE_XML")]
        private void TraceXmlParcel(string message, string kind)
        {
            if (String.IsNullOrEmpty(message)) return;
            try
            {
                var xreader = XmlReader.Create(new StringReader(message), xmlrs);
                var xmlDoc = new System.Xml.XmlDocument();
                xmlDoc.Load(xreader);

                // display the raw message
                var builder = new System.Text.StringBuilder();
                // first, reformat it into a better string, indented, etc.
                using (var writer = System.Xml.XmlWriter.Create(builder, xmlws))
                {
                    xmlDoc.Save(writer);
                }

                // now write it to the file
                var s = builder.ToString();
                File.WriteAllText("c:\\dev\\vsp\\C4Test\\ML2\\" + kind + ".xml",
                                  s,
                                  System.Text.Encoding.UTF8);
            }
            catch (System.Exception /* exc1 */)
            {
                // eh, we gave it out best shot
            }
        }

    }
}
